Android NDK

您所在的位置:网站首页 下列对于android ndk Android NDK

Android NDK

2023-09-05 20:51| 来源: 网络整理| 查看: 265

文章大纲 引言一、在Linux下通过gcc/g++编译运行C/C++1、编译的基本流程2、在Linux环境下通过gcc/g++把C/C++源文件,编译成可执行的文件(CPU指令集)。 二、通过NDK进行交叉编译1、指定要链接的头文件和库文件2、打包成配置成静态库和动态库2.1、静态库2.2、动态库2.3、Linux 编译创建静态库和动态库2.4、Linux 使用静态库和动态库2.5、Android 5.0以以下 与Android6.0及以上版本关于动态库的依赖链接区别 三、CPU架构类型APP_ABI四、配置Android Studio NDK 项目1、通过ndkBuild使用Android.mk方式进行配置1.1、在Module目录下的Gradle脚本android里的defaultConfig节点下添加externalNativeBuild配置cpu的eabi架构1.2、编写自己的Android.mk文件1.3、在Module目录下的Gradle脚本android里的与defaultConfig节点同级的节点添加 2、通过CMakeList方式进行配置1.1、在Module目录下的Gradle脚本android里的defaultConfig节点下添加externalNativeBuild配置cpu的eabi架构1.2、编写自己的CMakeList.txt文件1.3、在Module目录下的Gradle脚本android里的与defaultConfig节点同级的节点添加externalNativeBuild 节点

引言

前一篇文章Android NDK——必知必会之Makefile和CMake基本使用语法概述(七)简单介绍了MakeFile和CMake的相关知识,这篇就从小结下在Linux下交叉编译的基本流程。

Android NDK——必知必会之配置Windows与Linux共享及 Linux NDK 交叉编译环境配置(一)Android NDK——必知必会之JNI和NDK基础全面详解(二)Android NDK——必知必会之JNI的C++操作函数详解和小结(三)Android NDK——必知必会之从Java 传递各种数据到C/C++进行处理全面详解(四)Android NDK——必知必会之C/C++传递各种类型数据到Java进行处理及互相调用详解(五)Android NDK——必知必会之Native线程操作及线程同步全面详解(六)Android NDK——必知必会之Makefile和CMake基本使用语法概述(七)Android NDK——必知必会之使用gcc/g++在Linux下完成交叉编译(八) 一、在Linux下通过gcc/g++编译运行C/C++ 1、编译的基本流程

一个C/C++源文件通常需要经过预处理(preprocessing)、编译(compilation)、**汇编(assembly)**和 链接(linking) 四个流程才会变成可以在系统平台上的可执行文件,但是并不意味着必须要执行四个过程所有对应的命令,以编译main.c为例: 这里写图片描述

预处理阶段主要处理include和define等,它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替。 //其中 -E的作用是让gcc在预处理结束后停止编译。 root@Mo-vm:/usr/local/src/share# gcc -E main.c -o mainclient.i root@Mo-vm:/usr/local/src/share# ls main.c mainclient.i 在编译阶段时,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。 // -S的作用是编译后结束,编译生成了汇编文件。 root@Mo-vm:/usr/local/src/share# gcc -S mainclient.i -o mainclient.s root@Mo-vm:/usr/local/src/share# ls main.c mainclient.i mainclient.s ​ 汇编阶段把 .s文件翻译成二进制机器指令文件.o,这个阶段接收.c, .i, .s的文件都没有问题。 root@Mo-vm:/usr/local/src/share# gcc -c mainclient.s -o mainclient.o root@Mo-vm:/usr/local/src/share# ls main.c mainclient.i mainclient.o mainclient.s 链接阶段是链接的是函数库(包含静态库和动态库),在main.c中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明,系统把这些函数实现都被做到名为libc.so的动态库。 root@Mo-vm:/usr/local/src/share# gcc -o mainclient mainclient.s root@Mo-vm:/usr/local/src/share# ls main.c mainclient mainclient.i mainclient.o mainclient.s root@Mo-vm:/usr/local/src/share# ./mainclient excute c in linux root@Mo-vm:/usr/local/src/share#

默认情况下,gcc编译器只会使用/lib和/usr/lib这两个目录下的库文件,如果存在一个so不在这两个目录,在编译时候就会出现找不到的情况,幸好还可以在**/etc/ld.so.conf文件中可以指定而外的编译链接库路径**,需要引入第三方的库时也可以在**/etc/ld.so.conf**文件里更新

include /etc/ld.so.conf.d/*.conf #引入其他的conf文件 /usr/local/lib #增加库搜索目录 #编辑完成后 使用 ldconfig 更新 2、在Linux环境下通过gcc/g++把C/C++源文件,编译成可执行的文件(CPU指令集)。

这里写图片描述 如上图所示,生成了两个可执行的文件(仅隶属于与当前编译环境所支持的CPU指令集),但是这两个都无法在Android平台上运行(要验证也很简单找一台root了的手机然后把生成的可执行文件push到手机里,接着就像在Linux执行一样直接用命令行运行即可),虽然Android系统也是基于Linux系统的,但是在Linux上编译出来的可执行文件仅支持当前的CPU,很明显不同平台的CPU所支持的指令集是不一样的(即C/C++不跨平台)。这也是前面我们介绍NDK目录下的子目录platforms下每个系统下都有不同架构的子目录,通过NDK提供的这些工具我们就可以编译对应架构的函数库。

#生成静态库 # -fPIC 产生与位置无关代码 #可能会被不同的进程加载到不同的位置上,如果共享对象中的指令使用了绝对地址。那么在共享对象被加载时就必须根据相关模块的加载位置对这个地址做调整,也就是修改这些地址,让它在对应进程中能正确访问,那么就不能实现多进程共享一份物理内存(无法动态共享) gcc -fPIC -c Test.c -o Test.o ar r libTest.a Test.o #生成动态库 gcc -fPIC -shared Test.c -o libTest.so #或者 gcc -fPIC -c Test.c #生成.o gcc -shared Test.o -o libTest.so #使用库 #默认优先使用动态库 gcc main.c -L. -lTest -o main #强制使用静态库 #-Wl 表示传递给 ld 链接器的参数 #最后的 -Bdynamic 表示 默认仍然使用动态库 gcc main.c -L. -Wl,-Bstatic -lTest -Wl,-Bdynamic -o main #使用动态库链接的程序,linux运行需要将动态库路径加入/etc/ld.so.conf #mac(dylib)和windows(dll)可以将动态库和程序(main)放在一起 #mac 中gcc会被默认链接到xcode的llvm,不支持上面的指定链接动态库 #查看可执行文件符号 nm main #打包 .a 到so #--whole-archive: 将未使用的静态库符号(函数实现)也链接进动态库 #--no-whole-archive : 默认,未使用不链接进入动态库 二、通过NDK进行交叉编译

所谓交叉编译可以简单理解为在一个平台环境下编译出另一个平台可运行的CPU 指令集的过程(这个概念仅仅是我个人的理解)而NDK 中的toolchains\xxx-linux-androideabi-4.9\prebuilt\windows-x86_64\bin目录则提供了一些列不同CPU 架构的交叉编译工具,我们只要合理配置使用即可,如下图所示: 这里写图片描述 以上图形只做了两步工作,配置局部环境变量(非必须步骤),使用NDK中提供的工具进行编译,原本我也以为就是需要使用对应的工作进行编译就可以了,而却提示找不到标准库中的头文件,这是因为在C/C++源码中默认链接到的标准库的(相当于是javac 中会有个classpath的环境变量),而当我们使用NDK中的工具进行编译的时候,需要使用NDK提供的对应的头文件,只要在执行编译命令的时候添加上对应的参数即可。所以总结起来使用NDK进行交叉编译主要步骤有:

1、指定要链接的头文件和库文件 /** *使用xx作为这一次编译的头文件与库文件的查找目录,配置了之后就会在编译的时候自动去查找XX目录下的usr/include的头文件和usr/lib目录下的库文件,如gcc --sysroot=目录1 main.c 编译时就会去 目录1/usr/include查找头文件和目录1/usr/lib找库文件(一般是.a和.so) */ --sysroot=XX /** *指定头文件查找目录,配置之后会自动覆盖--sysroot中的头文件查找目录,查找XX/usr/include的头文件,如gcc --sysroot=目录1 -isysroot 目录2 main.c 编译时就不会去 目录1/usr/include下找头文件而是去 目录2/user/include下查找头文件和到目录1/usr/lib下查找库文件 */ -isysroot XX /** *指定头文件查找目录,配置之后会直接查找查找XX目录下的头文件,如gcc --sysroot=目录1 -isysroot 目录2 -isystem 目录3 main.c 编译时就不会去 目录1/usr/include下找头文件而是到 目录2/user/include和目录3下查找头文件和到目录1/usr/lib下查找库文件 */ -isystem XX /** *指定头文件查找目录,配置之后会直接查找查找XX目录下的头文件,如gcc --sysroot=目录1 -isysroot 目录2 -isystem 目录3 -I 目录4 main.c 编译时就不会去 目录1/usr/include下找头文件而是到 目录2/user/include和目录3及目录4下查找头文件和到目录1/usr/lib下查找库文件 */ -I XX /*****其中优先级: -I > -isystem > -isysroot所谓优先级指的是同时配置了这些指令的话编译时会先到优先级大的目录下查找,找到了就不会去查找其他目录的头文件了,所以以上的注释都是在优先级高的找不到的时候再继续往低一级的查找,而且不会去递归去查找指定目录下的子目录********/ /** *链接指定XX目录下的xx.so,gcc -L目录1 -l库名,比如连接到NDK的日志库和libEGL库可以使用gcc -LC:\AndroidIDE\SDK\ndk-bundle\platforms\android-26\arch-arm\usr\lib -llog.so -lEGL.so(当然也可以使用--sysroot来指定) */ -L:XX -lxx.so

只要在编译时配置上对应的头文件和库文件目录,即可编译出能成功在Android上可执行的文件,不过这里需要注意的是不同版本的NDK 编译的指令有所不同,我的是在NDK_r16b下编译的,一开始使用 -isystem /usr/local/ndk/android-ndk-r16b/sysroot/usr/include/arm-linux-androideabi/asm 配置时编译报asm/types.h: No such file or directory,原因就是因为 -isystem /usr/local/ndk/android-ndk-r16b/sysroot/usr/include/arm-linux-androideabi/asm这样配置的时候,就回去xxx/asm下去找, 而代码中是include asm/types.h 这样引入的就会去 xxx/asm/asm/types.h查找所以自然出错。简单来说:指定为xxxx, include asm/types.h 就是去xxxx/asm/types.h

这里写图片描述

2、打包成配置成静态库和动态库

库本质上是一种可执行的二进制代码,可以被OS载入到内存中去执行,根据载入内存的时间和流程可以分为静态库(Linux中后缀名为”.a”)和动态库(Linux中后缀名为”.so”)两种:

2.1、静态库

静态库包含了所有执行的代码,库中所有的函数机器码在编译链接时全部被拷贝到可执行的文件中并被添加到和它链接的每一个程序中,简单来说就是把库文件的代码全部加入到可执行文件中,因此生成的文件比较大(打包成APK也较大),但在运行时也就不再需要库文件了,所以静态库节省时间,不需要再进行动态链接,需要调用的代码直接就在代码内部。不过当静态库中某一个函数改变生活,所有使用这个静态库的程序都得重新编译,适用于内部使用。

2.2、动态库

动态库在编译链接时并没有把库文件的全部代码加入到可执行文件中,而是在程序运行时由运行时的链接文件加载库,即运行时再动态申请调用进行链接拷贝,gcc在编译时默认使用动态库,动态库能节省空间,如果一个动态库被两个程序调用,那么这个动态库只需要在内存中即可(因为加载器在加载动态库时,OS会先检查对应的动态库是否已经因为其他程序把这个动态库信息加载到了内存中,若没有加载到内存中,OS会将动态库载入内存,并将它的引用计数设置为1;如果已经加载到内存,仅将动态库的引用计数加1,无需重新加载),而且Java在不经过封装的情况下只能直接使用动态库,因此动态库又称为共享库。

2.3、Linux 编译创建静态库和动态库

ldd命令可以查看一个可执行程序依赖的共享库 ldd + 路径/可执行程序

编译创建静态库和动态库的步骤:

首先创建.o文件,可以把java源文件通过编译命令先生成.o文件(编译创建动态库时需要要使用-fpic选项)

“-fpic”的作用是告诉gcc产生的代码不要包含对函数和变量具体内存位置的引用,因为现在还无法知道使用该代码的应用程序会将它连接到哪一段内存地址空间。只有这样编译出的xxx.o才可以被用于建立共享链接库。

ar -cr lib库名.a * .o 创建静态库 或者 gcc -shared -o lib库名.so * .o创建动态库 2.4、Linux 使用静态库和动态库

对于静态库:

若静态库放在系统目录**/lib或者/usr/lib**下则执行方式为 gcc xxxx.c -l库名若静态库没放在系统目录下则执行方式为执行 gcc xxxx.c -L 静态库路径 -l库名或者gcc xxxx.c 路径/整个静态库(大写i)

对于动态库:

若动态库存放在系统目录**/lib或者/usr/lib**下则执行方式为 gcc xxxx.c -l库名若动态库没有放在系统目录下则先执行 gcc main.c -L 动态库路径 -l 库名

不过这种方式在执行可执行程序的时候可能出现如下错误 ./a.out: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

解决步骤如下

在/etc/bash.bashrc文件中添加export LD_LIBRARY_PATH=$LD_LRBRARY_PATH:动态库的绝对路径。后source /etc/bash.bashrc

在/etc/ld.so.conf.d目录下创建一个以.conf为后缀的文件,在这个文件中添加上动态库的绝对路径,后执行sudo ldconfig

ldconfig命令的用途,主要是在默认搜寻目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索出可共享的动态链接库.

如果第一种的方式都配置完之后出现如下错误

/usr/bin/ld: cannot find -laddsub collect2: ld returned 1 exit status

这种错误说明编译器找不到需要链接的库,则还需要执行gcc xxxx.c 动态库完整路径/动态库。

2.5、Android 5.0以以下 与Android6.0及以上版本关于动态库的依赖链接区别

Android 5.0及以下与6.0及以上时需要注意:若存在两个动态库libhello-jni.so 与 libTest.so。libhello-jni.so依赖于libTest.so (使用NDK下的ndk-depends可查看依赖关系),则:

//=6.0: System.loadLibrary("hello-jni");

比如在已经编译好的静态库clib.a(或者动态库clib.so)中有一个test方法,而现在需要在另一个源文件crazy.c中使用clib.a中的test2方法中使用clib.a中的test方法,最后再把crazy.c打包成一个静态库或者动态库crazy.a或者crazy.so,如果clib是被以静态库引用的话则最终的crazy.a或者crazy.so库都会存在clib.a的所有方法实现(因为编译时就将所有符号加载到输出库),反之则只存在crazy.c的方法实现而不存在clib中的test方法(因为使用动态库是通过在运行时动态加载动态库的方法)

三、CPU架构类型APP_ABI

不同架构的CPU的指令集有所不同,不同 Android 设备可能会使用不同的 CPU,因此支持不同的指令集而需要生成的不同的CPU架构(ndk r17 只支持:armeabi-v7a, arm64-v8a, x86, x86_64)

指令集值基于 ARMv7 的设备上的硬件 FPU 指令APP_ABI := armeabi-v7aARMv8 AArch64APP_ABI := arm64-v8aIA-32APP_ABI := x86Intel64APP_ABI := x86_64MIPS32APP_ABI := mipsMIPS64 (r6)APP_ABI := mips64所有支持的指令集APP_ABI := all

查看Android设备 CPU架构的命令 adb shell cat /proc/cpuinfo adb shell getprop ro.product.cpu.abi

apk在安装的时候,如果手机是armeabi-v7a的,则会首先查看apk中是否存在armeabi-v7a目录,如果没有就会查找armeabi。所以必须保证cpu目录下so数量一致,如果目标是armeabi-v7a,但是拥有一个armeabi的,也可以把它放到armeabi-v7a目录下,但是反过来不行。

ABI(横 so)/CPU(竖 手机)armeabiarmeabi-v7aarm64-v8ax86x86_64ARMV5支持ARMV7支持支持ARMV8支持支持支持X86支持X86_64支持支持 四、配置Android Studio NDK 项目

如果一开始新建Module时没有配置支持NDK,也可以在中途自己配置支持NDK,因为Gradle脚本中各种节点名称是BaseExtension.class中对应的方法名称,通过Groovy语法编写。 在这里插入图片描述 另外如果不知道各节点可以配置哪些属性,可以通过Java的形式来确定,其实就是Java对象的成员变量:

externalNativeBuild.ndkBuild(new Action() { @Override void execute(CoreExternalNativeCmakeOptions ndkBuildOptions) { ndkBuildOptions.abiFilters=... ndkBuildOptions.targets=... } })

需要注意的是同一节点所在的层次不一样效果也会有所不同,比如在defaultConfig节点中的 externalNativeBuild的作用是指导编译源文件,而与defaultConfig同级的externalNativeBuild的作用是配置Native 构建监本的路径。

1、通过ndkBuild使用Android.mk方式进行配置 import com.android.build.gradle.internal.dsl.CoreExternalNativeCmakeOptions import com.android.build.gradle.internal.dsl.NdkBuildOptions apply plugin: 'com.android.application' android { compileSdkVersion 28 defaultConfig { ... // 指导我们的 源文件 编译 externalNativeBuild{ ndkBuild{ // armeabi-v7a abiFilters "armeabi-v7a" } } // (注释部分的Java形式为说明原理Gradle里貌似不直接支持) // externalNativeBuild.ndkBuild(new Action() { // @Override // void execute(CoreExternalNativeCmakeOptions ndkBuildOptions) { // // 你希望编译你的 c/c++ 源文件 编译几种cpu(arm、x86 ) // ndkBuildOptions.abiFilters.add("armeabi-v7a") // ndkBuildOptions.abiFilters.add("x86") // } // }) //externalNativeBuild.cmake // 应该打包几种cpu,比如: 集成了第三方库 ,第三方库中提供了 arm的 提供了 x86的可以在此处 指导 只打包 arm,生成出来的apk 就只会包含 arm的 ndk{ abiFilters "armeabi-v7a" } } // externalNativeBuild.cmake //配置 native 的编译脚本路径 // externalNativeBuild.ndkBuild(new Action() { // @Override // void execute(NdkBuildOptions ndkBuildOptions) { // ndkBuildOptions.path = "src/main/cpp/Android.mk" // } // }) externalNativeBuild{ ndkBuild{ path "Android.mk" } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:28.0.0-rc01' } 1.1、在Module目录下的Gradle脚本android里的defaultConfig节点下添加externalNativeBuild配置cpu的eabi架构 android { compileSdkVersion 26 ... defaultConfig { ... externalNativeBuild{ ndkBuild{ //设置源文件只编译armeabi-v7a 架构的 abiFilters 'armeabi-v7a' } } } ndkBuild{ //设置只打包出包含armeabi-v7a 架构的apk abiFilters 'armeabi-v7a' } } } 1.2、编写自己的Android.mk文件

Android.mk

#源文件在的位置。宏函数 my-dir 返回当前目录(包含 Android.mk 文件本身的目录)的路径。 LOCAL_PATH := $(call my-dir) $(info "LOCAL_PATH:======== ${LOCAL_PATH}") #引入其他makefile文件。CLEAR_VARS 变量指向特殊 GNU Makefile,可为您清除许多 LOCAL_XXX 变量 #不会清理 LOCAL_PATH 变量 include $(CLEAR_VARS) #存储您要构建的模块的名称 每个模块名称必须唯一,且不含任何空格 #如果模块名称的开头已是 lib,则构建系统不会附加额外的前缀 lib;而是按原样采用模块名称,并添加 .so 扩展名。 LOCAL_MODULE := abc #包含要构建到模块中的 C 和/或 C++ 源文件列表 以空格分开 LOCAL_SRC_FILES := native-lib.c \ c.c #构建动态库 include $(BUILD_SHARED_LIBRARY) 1.3、在Module目录下的Gradle脚本android里的与defaultConfig节点同级的节点添加 android { compileSdkVersion 26 ... defaultConfig { ... externalNativeBuild{ ndkBuild{ abiFilters 'armeabi-v7a' } } } } ... externalNativeBuild{ ndkBuild{ //配置指向Android.mk文件的路径,如:path "Android.mk"代表与module下的build.gradle脚本同目录 path "xxxx/xxx/Android.mk" } } } } 2、通过CMakeList方式进行配置 1.1、在Module目录下的Gradle脚本android里的defaultConfig节点下添加externalNativeBuild配置cpu的eabi架构 android { compileSdkVersion 26 ... defaultConfig { ... externalNativeBuild{ ndkBuild{ abiFilters 'armeabi-v7a' } } } } } 1.2、编写自己的CMakeList.txt文件 cmake_minimum_required(VERSION 3.4.1) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. file(GLOB LIBGIF_SOURCE src/main/cpp/*.c src/main/cpp/*.cpp) set(LIBNAME crazygif) add_library( # Sets the name of the library. ${LIBNAME} # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). ${LIBGIF_SOURCE}) include_directories(src/main/cpp) target_link_libraries( # Specifies the target library. ${LIBNAME} # Links the target library to the log library included in the NDK. jnigraphics log ) 1.3、在Module目录下的Gradle脚本android里的与defaultConfig节点同级的节点添加externalNativeBuild 节点 externalNativeBuild { cmake { path "CMakeLists.txt" } }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3